﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using System.Linq;
using BeanboxSoftware.BeanProxy.Internals;
using System.Reflection.Emit;

namespace BeanboxSoftware.BeanProxy
{
  /// <summary>
  /// Holds detailed configurations on one specific method of a proxy object.
  /// Typically you would use ProxyManager's get and lookup methods to find a handle
  /// on a MethodConfig of a method of a proxy.
  /// </summary>
  public partial class MethodConfig
  {
    /// <summary>
    /// Default constructor, note that the resultant method config is not ready for use
    /// You need to call Initialize() before using
    /// </summary>
    public MethodConfig()
    {
      _isReal = null;
      Count = 0;
      NextCallbackIndex = 0;
      Behavior = CallbackBehavior.Repeat;
      Callbacks = new List<MulticastDelegate>();
      //Access = new MethodConfigAccess(this);
    }

    private bool? _isReal;
    /// <summary>
    /// Set/get whether or not invoking the wrapped method calls the base method (real), or
    /// steps in a fake callback (not real)
    /// </summary>
    public bool IsReal
    {
      get
      {
        if (_isReal.HasValue)
        {
          return _isReal.Value;
        }

        throw new NotImplementedException(Method.DeclaringType.Name + "." + Method.Name + " has not been configured.");
      }
      set
      {
        _isReal = value;

        if (!value && Callbacks.Count == 0)
        {
          object retVal = null;
          if (Method.ReturnType != typeof(void) && Method.ReturnType.IsValueType)
          {
            retVal = BeanboxSoftware.BeanProxy.Internals.BuilderBase.Default(Method.ReturnType);
          }
          Add(args => retVal);
        }
      }
    }

    /// <summary>
    /// The number of times the method was called (includes both real and not real calls)
    /// </summary>
    public int Count { get; set; }

    public int NextCallbackIndex { get; set; }

    /// <summary>
    /// Defines the style in which the Multicast delegation of the callback is invoked.
    /// </summary>
    public CallbackBehavior Behavior { get; set; }

    public List<MulticastDelegate> Callbacks { get; protected set; }

    private object InvokeNextCallback(params object[] args)
    {
      try
      {
        var callback = NextCallback(args);

        object[] wrapped = WrapAsNeeded(callback.Method, args);

        return callback.Method.Invoke(callback.Target, wrapped);
      }
      catch (TargetInvocationException targetException)
      {
        throw targetException.InnerException;
      }
    }

    public object Invoke(params object[] args)
    {
      try
      {
        return Method.Invoke(Target, args);
      }
      catch (TargetInvocationException tie)
      {
        throw tie.InnerException;
      }
    }

    protected object[] WrapAsNeeded(MethodInfo methodInfo, object[] args)
    {
      var pInfos = methodInfo.GetParameters();

      if (pInfos.Length != 1)
      {
        return args;
      }

      if (args.Length != 1)
      {
        return new object[] { args };
      }

      //now we know both pInfos AND args are length 1

      ParameterInfo pInfo = pInfos[0];
      object arg = args[0];

      if (pInfo.ParameterType == typeof(object[]) && pInfo.ParameterType != arg.GetType())
      {
        return new object[] { args };
      }

      return args;
    }

    protected MulticastDelegate NextCallback(params object[] args)
    {
      var ruleCallback = Target.MethodGroup.RuleIntercept(this, args);
      if (ruleCallback != null) //there might be a rule - rules take precidence
      {
        return ruleCallback;
      }

      if (Callbacks.Count == 0)
      {
        return null;
      }

      int next = NextCallbackIndex;

      switch (Behavior)
      {
        case CallbackBehavior.Repeat:
          next = (next + 1) % Callbacks.Count;
          break;
        case CallbackBehavior.RepeatLast:
          next = Math.Min(next + 1, Callbacks.Count - 1);
          break;
        case CallbackBehavior.RunOnce:

          next++;

          if (next > Callbacks.Count)
          {
            throw new InvalidOperationException(
              string.Format(
                "Too many callbacks were invoked. Callback count: {0}. Callback behavior: {1}",
                Callbacks.Count,
                Behavior.ToString()));
          }

          break;
      }

      var md = Callbacks[NextCallbackIndex];
      NextCallbackIndex = next;

      return md;
    }

    /// <summary>
    /// Removes all defined callback methods
    /// </summary>
    public void ClearCallbacks()
    {
      Callbacks.Clear();
      NextCallbackIndex = 0;
    }

    public void Add(Func<object[], object> genericCallback)
    {
      Add((MulticastDelegate)genericCallback);
    }
    public void Add(MulticastDelegate c)
    {
      if (c == null)
      {
        ClearCallbacks();
        _isReal = null;
      }
      else
      {
        var delegates =
          (from del in c.GetInvocationList()
           select (MulticastDelegate)del).ToList();

        //a delegate pointing to itself would never work in reality
        if (delegates.Count > 1 && delegates[0].Method.Name.StartsWith(PreserverPrefix))
        {
          delegates.RemoveAt(0);
        }
        else
        {
          ClearCallbacks();
        }

        Callbacks.AddRange(delegates);
        _isReal = false;
      }
    }

    public List<object[]> ParameterHistory { get; private set; }
    protected object[] GenericParameters { get; private set; }

    public object Intercept(params object[] args)
    {
      for (int i = 0; i < GenericParameters.Length; i++)
      {
        GenericParameters[i] = i < args.Length ? args[i] :
          BeanboxSoftware.BeanProxy.Internals.BuilderBase.Default(Parms[i].ParameterType);
      }

      ParameterHistory.Add(args);

      return InvokeNextCallback(args);
    }

    public object GetParameter(int index)
    {
      return GenericParameters[index];
    }

    public MethodInfo Method { get; set; }
    public IProxy Target { get; set; }
    protected ParameterInfo[] Parms { get; set; }

    public void Initialize(object proxy, MethodInfo method)
    {
      ParameterHistory = new List<object[]>();
      Target = proxy as IProxy;

      Method = method ?? ((Func<object[], object>)(Invoke)).Method;

      Parms = Method.GetParameters();

      GenericParameters = new object[Parms.Length];

      if (method == null)
      {
        _isReal = true; //used for unconfigured proxies
      }
      else if (Target != null)
      {
        switch (Target.DefaultBehavior)
        {
          case DefaultBehavior.Fake:
            IsReal = false;
            break;
          case DefaultBehavior.Real:
            _isReal = true;
            break;
          case DefaultBehavior.ThrowException:
            _isReal = null;
            break;
        }
      }
    }

    /// <summary>
    /// Returns a method info for a Delegate (where T : delegate)
    /// </summary>
    /// <typeparam name="T">Delegate type</typeparam>
    /// <returns>method info</returns>
    public static MethodInfo GetInvoker<T>()
    {
      return GetInvoker(typeof(T));
    }
    /// <summary>
    /// Returns a method info for a Delegate type
    /// </summary>
    /// <param name="delegateType">typeof(T) where T : delegate</param>
    /// <returns>method info</returns>
    public static MethodInfo GetInvoker(Type delegateType)
    {
      if (!delegateType.IsSubclassOf(typeof(Delegate)))
      {
        throw new ArgumentException("T in LookupMethod must be a Delegate type");
      }

      var invoker = delegateType.GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public);

      return invoker;
    }

    public static readonly string PreserverPrefix = "___beanbox_software_method_config_multicast_prefix___";
  }

  /// <summary>
  /// A MethodConfig of a wrapped method of type C
  /// </summary>
  /// <typeparam name="C">MulticastDelegate</typeparam>
  public partial class MethodConfig<C>
  {
    public MethodConfig Access { get; protected set; }

    public CallbackBehavior Behavior { get { return Access.Behavior; } set { Access.Behavior = value; } }
    public void ClearCallbacks()
    {
      Access.ClearCallbacks();
    }
    public int Count { get { return Access.Count; } set { Access.Count = value; } }
    public bool IsReal { get { return Access.IsReal; } set { Access.IsReal = value; } }

    private C _preserver;
    /// <summary>
    /// Append a proxy method
    /// </summary>
    public C Callback
    {
      get { return Access.Callbacks.Count == 0 ? default(C) : _preserver; }
      set { Access.Add((MulticastDelegate)((object)value)); }
    }

    public C Invoke
    {
      get;
      protected set;
    }

    public MethodConfig(MethodConfig access)
    {
      Access = access;

      var parms = new List<ParameterInfo>(access.Method.GetParameters());

      var dm = new DynamicMethod(MethodConfig.PreserverPrefix + access.Method.Name,
        access.Method.ReturnType,
        parms.ConvertAll<Type>(pi => pi.ParameterType).ToArray(),
        false);

      var gen = dm.GetILGenerator();
      gen.ThrowException(typeof(Exception));

      var del = dm.CreateDelegate(typeof(C));
      _preserver = (C)((object)del);

      del = Delegate.CreateDelegate(typeof(C), access.Target, access.Method, false);
      Invoke = (C)((object)del);
    }

    protected void Add(C c)
    {
      Access.Add((MulticastDelegate)((object)c));
    }
  }
}
